In Denial - Debugging STATUS_ACCESS_DENIED
(By: The NT Insider, Vol 13, Issue 2, March - April 2006 | Published: 17-Apr-06| Modified: 17-Apr-06)
Being the good citizen that I am, I decided to put my driver through a round of DC2 testing. Much to my surprise, DC2 ran and exited fast. As a matter of fact, it ran a little too fast, so I decided to fire up IrpTracker to see what sort of hideous I/O it was sending to my driver. Much to my surprise I was met with an entirely empty display. The DC2 test resulted in zero IRPs being sent to my device, which is not exactly useful.
At this point I flipped on the native API tracking feature of IrpTracker, which resulted in a new mystery - every single ZwCreateFile call made by DC2 failed within the I/O manager with STATUS_ACCESS_DENIED.
The create operation's parameters looked reasonable enough, so I decided to duplicate one of the creates in a test application. This, of course, worked perfectly fine, so it was time to find the point of failure within the I/O Manager and figure out what was going on.
Security in Windows is extremely well documented. Therefore, I will not attempt to review or summarize that documentation (an afternoon with Google will be far more beneficial than a couple of wasted paragraphs here). The point of this article is to demonstrate how to extract security information from the target system via a debugger, not how security works in general.
SeAccessCheck
The mother of all security functions in Windows is SeAccessCheck, which is the routine within the O/S used to determine if a particular caller has a particular access to a particular object. It was a safe bet that this routine was involved in DC2's inability to open my device, so I tried starting there:
0: kd> bp nt!seaccesscheck
0: kd> g
If you try this you'll quickly find that SeAccessCheck is one busy function. It's called practically non-stop from all over the O/S to validate access to things like files and registry keys. Clearly, putting a breakpoint at this high of a level was going to be entirely futile.
Back Up A Little
I find WinDbg to be a bit fussy with conditional breakpoints, so conditionalizing the SeAccessCheck breakpoint was out of the question. Instead, I decided to try and find the path through the Object Manager that resulted in the call to SeAccessCheck for named device objects. I figured there couldn't possibly be that many direct device opens going on (or opens of files on devices with the FILE_DEVICE_SECURE_OPEN bit set), so I was hopeful that this would yield better results.
Based on what I knew from other articles such as "Meandering through the Object Manager" (The NT Insider, March-April 2005), I knew IopParseDevice was the best place to start my searching. While this routine is called fairly often, it's far more manageable than SeAccessCheck. After a few attempts, I finally broke in on the correct stack (i.e. DC2 opening my device), and was ready to begin debugging (See Figure 1).
1: kd> kb
Args to Child
00000000 ae4bfc20 00000040 nt!IopParseDevice
0006f1a8 00000000 00000001 nt!ObOpenObjectByName+0x13c
0020ff88 02000000 0006f1a8 nt!IopCreateFile+0x5d4
0020ff88 02000000 0006f1a8 nt!IoCreateFile+0x38
0020ff88 02000000 0006f1a8 nt!NtCreateFile+0x31
0020ff88 02000000 0006f1a8 nt!KiFastCallEntry+0x12c
01012438 0020ff88 02000000 ntdll!KiFastSystemCallRet
0020ff88 02000000 0006f1a8 ntdll!ZwCreateFile+0xc
0006f1a8 0020ffec 77f2809e dc2+0x12438
0006f1a8 0020e543 00000000 kernel32!BaseThreadInitThunk+0xe
010123b0 0006f1a8 00000000 ntdll!_RtlUserThreadStart+0x23
1: kd> dt nt!_object_attributes 0006f1a8
+0x000 Length : 0x18
+0x004 RootDirectory : (null)
+0x008 ObjectName : 0x0006f1c0 _UNICODE_STRING "\device\00000036"
+0x00c Attributes : 0x40
+0x010 SecurityDescriptor : (null)
+0x014 SecurityQualityOfService : (null)
Figure 1 - Getting Started |
Back to SeAccessCheck
At this point I had two choices:
1. Begin stepping through the debugger until the magic status 0xC0000022 (STATUS_ACCESS_DENIED) showed up in the disassembly.
2. Reenable my SeAccessCheck breakpoint, hit GO, and hope I get lucky.
Since I try to avoid long, drawn out sessions of staring at assembly in the debugger, I decided to go with option two:
1: kd> bp nt!seaccesscheck
1: kd> g
Breakpoint 1 hit
nt!SeAccessCheck:
814a2320 8bff mov edi,edi
I immediately hit my breakpoint, and was pleased to see that it was indeed the correct call (See Figure 2).
1: kd> kb
Args to Child
88289ee0 844967d4 00000001 nt!SeAccessCheck
828208b0 00000000 844967b8 nt!IopParseDevice+0x63d
00000000 ae4bfc20 00000040 nt!ObpLookupObjectName+0x609
0006f1a8 00000000 00000001 nt!ObOpenObjectByName+0x13c
0020ff88 02000000 0006f1a8 nt!IopCreateFile+0x5d4
0020ff88 02000000 0006f1a8 nt!IoCreateFile+0x38
0020ff88 02000000 0006f1a8 nt!NtCreateFile+0x31
0020ff88 02000000 0006f1a8 nt!KiFastCallEntry+0x12c
01012438 0020ff88 02000000 ntdll!KiFastSystemCallRet
0020ff88 02000000 0006f1a8 ntdll!ZwCreateFile+0xc
0006f1a8 0020ffec 77f2809e dc2+0x12438
0006f1a8 0020e543 00000000 kernel32!BaseThreadInitThunk+0xe
010123b0 0006f1a8 00000000 ntdll!_RtlUserThreadStart+0x23
Figure 2 - Now We're Getting Somewhere |
Moment of Truth
Next came the exciting part, "Had I guessed correctly and was my problem buried somewhere within SeAccessCheck?" Before going further, it was time to refresh my memory of the the SeAccessCheck prototype. As luck would have it, this process is fully documented (See Figure 3).
BOOLEAN
SeAccessCheck(
IN PSECURITY_DESCRIPTOR SecurityDescriptor,
IN PSECURITY_SUBJECT_CONTEXT SubjectSecurityContext,
IN BOOLEAN SubjectContextLocked,
IN ACCESS_MASK DesiredAccess,
IN ACCESS_MASK PreviouslyGrantedAccess,
OUT PPRIVILEGE_SET *Privileges OPTIONAL,
IN PGENERIC_MAPPING GenericMapping,
IN KPROCESSOR_MODE AccessMode,
OUT PACCESS_MASK GrantedAccess,
OUT PNTSTATUS AccessStatus);
Figure 3 - SeAccessCheck Prototype |
According to the documentation, this routine will return TRUE if the caller described by the SubjectSecurityContext is allowed access to the object described by the SecurityDescriptor (more info on these parameters later), and FALSE otherwise. The target machine was an x86 machine, so I stepped out of the call and checked EAX for the result:
1: kd> bp /1 /c @$csp @$ra;g
Breakpoint 2 hit
nt!IopParseDevice+0x63d:
81565557 3245e3 xor al,[ebp-0x1d]
1: kd> r @eax
eax=00000000
Aha! This revealed that SeAccessCheck was indeed failing. Now it was a matter of figuring out why.
Three Easy Pieces
I needed to extract the following three pieces of information from the debugger in order to determine why this access check failed:
1. The desired access of the caller
2. The Discretionary Access Control List (DACL) of the object
3. The security token of the caller
Luckily the first one was a gimme since DesiredAccess is the second parameter to ZwCreateFile. In my case this meant the caller wanted 0x02000000, which is MAXIMUM_ ALLOWED. Now I needed to determine who had what access to the object, so it was time to move on to the DACL for the target device.
Displaying the Device's DACL
The first parameter to the SeAccessCheck call is the SECURITY_DESCRIPTOR of the object being checked. For lack of a better term, a SECURITY_DESCRIPTOR, well, describes the security of an object. Part of this security description is the DACL of the device, which is what I was looking for.
Luckily, there is already a !sd command that nicely parses one of these structures, so the only thing I had to do was find the address of the SECURITY_DESCRIPTOR. Since I knew this was the first parameter that was passed to SeAccessCheck, I used the information from the earlier stack dump I had (See Figure 4).
Args to Child
88289ee0 844967d4 00000001 nt!SeAccessCheck
1: kd> !sd 88289ee0
...
->Owner : S-1-5-32-544
->Group : S-1-5-18
...
->Dacl : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl : ->Ace[0]: ->AceFlags: 0x0
->Dacl : ->Ace[0]: ->AceSize: 0x18
->Dacl : ->Ace[0]: ->Mask : 0x001f01ff
->Dacl : ->Ace[0]: ->SID: S-1-5-32-544
->Dacl : ->Ace[1]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl : ->Ace[1]: ->AceFlags: 0x0
->Dacl : ->Ace[1]: ->AceSize: 0x14
->Dacl : ->Ace[1]: ->Mask : 0x001f01ff
->Dacl : ->Ace[1]: ->SID: S-1-5-18
->Dacl : ->Ace[2]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl : ->Ace[2]: ->AceFlags: 0x0
->Dacl : ->Ace[2]: ->AceSize: 0x14
->Dacl : ->Ace[2]: ->Mask : 0x001f01ff
->Dacl : ->Ace[2]: ->SID: S-1-5-20
...
Figure 4 - Finding the Address of the SECURITY_DESCRIPTOR |
According to the DACL contained within this security descriptor, three principals have access to this object:
1. BUILTIN\ADMINISTRATORS (S-1-5-32-544) have FILE_ALL_ACCESS (0x001f01ff)
2. NT AUTHORITY\SYSTEM (S-1-5-18) has FILE_ALL_ACCESS (0x001f01ff)
3. NT AUTHORITY\NETWORK SERVICE has FILE_ALL_ACCESS (0x001f01ff)
Luckily, this was exactly what I expected. This driver's INF file had locked down its named device objects by using this SDDL string:
D:P(A;;GA;;;BA)(A;;GA;;;SY)(A;;GA;;;NS)
If you refer to the SDDL documentation in the SDK you will see that this string exactly describes the access applied to the object as seen in the debugger.
Other Ways to Get the DACL
There are at least two other ways of finding and displaying the DACL of an object in the debugger:
1. If the object is a device object, !devobj will display the address of the DACL, if there is one. !acl can then be used to display the DACL in the debugger. Note that DACLs are only applied to the named device objects in the stack.
2. You can find the SECURITY_DESCRIPTOR of an object by using !object, then examine the OBJECT_HEADER. See Determining the ACL of an Object in the WinDBG documentation for detailed instructions.
Getting the Token
The last piece of this puzzle was the token that was being used by the caller. By comparing the information in the caller's token with the information in the DACL of the device object, I was able to determine why the Security Reference Monitor (SRM) was denying the open.
Much like everything else in WinDBG, there are a few ways at getting this info. However, to get the clearest possible picture of what was going on, I decided to look at the SECURITY_ SUBJECT_CONTEXT passed by IopParseDevice to SeAccessCheck. Since this was the second parameter to the function, I again used the saved stack information (See Figure 5).
1: kd> kb
Args to Child
88289ee0 844967d4 00000001 nt!SeAccessCheck
1: kd> dt nt!_security_subject_context 844967d4
nt!_SECURITY_SUBJECT_CONTEXT
+0x000 ClientToken : 0xb3489af0
+0x004 ImpersonationLevel : 3 ( SecurityDelegation )
+0x008 PrimaryToken : 0xaab283b0
+0x00c ProcessAuditId : 0x000002cc
Figure 5 - Checking Out SECURITY_SUBJECT_CONTEXT |
This was actually a fairly interesting SECURITY_SUBJECT_CONTEXT. The fact that ClientToken was non-NULL meant the thread making this call was impersonating, which means it was not running with the original token it inherited from its parent process.
Even though it was not strictly necessary for the original analysis, listed below is the thread's original token (i.e. PrimaryToken) that was shown by the debugger's !token command (See Figure 6).
1: kd> !token 0xaab283b0
_TOKEN aab283b0
TS Session ID: 0x3
User: S-1-5-21-1481837369-2907903028-1708812341-500
Groups:
00 S-1-5-21-1481837369-2907903028-1708812341-513
Attributes - Mandatory Default Enabled
01 S-1-1-0
Attributes - Mandatory Default Enabled
02 S-1-5-32-544
Attributes - Mandatory Default Enabled Owner
03 S-1-5-32-545
Attributes - Mandatory Default Enabled
04 S-1-5-4
Attributes - Mandatory Default Enabled
05 S-1-5-11
Attributes - Mandatory Default Enabled
06 S-1-5-15
Attributes - Mandatory Default Enabled
07 S-1-5-5-0-35361231
Attributes - Mandatory Default Enabled LogonId
08 S-1-2-0
Attributes - Mandatory Default Enabled
09 S-1-5-64-10
Attributes - Mandatory Default Enabled
10 S-1-16-12288
Attributes - GroupIntegrity GroupIntegrityEnabled
11 S-1-16-12288
Attributes - GroupIntegrity GroupIntegrityEnabledDesktop
Primary Group: S-1-5-21-1481837369-2907903028-1708812341-513
Privs:
00 0x00000001e Unknown Privilege Attributes - Enabled Default
01 0x000000017 SeChangeNotifyPrivilege Attributes - Enabled Default
...
09 0x000000014 SeDebugPrivilege Attributes - Enabled
...
19 0x00000001d SeImpersonatePrivilege Attributes - Enabled Default
...
22 0x000000021 SeIncreaseWorkingSetPrivilege Attributes - Authentication ID: (0,21b91ea)
Impersonation Level: Anonymous
TokenType: Primary
Source: User32 TokenFlags: 0x81 ( Token in use )
Token ID: 21d530a ParentToken ID: 0
Modified ID: (0, 21d53be)
RestrictedSidCount: 0 RestrictedSids: 00000000
Figure 6 - The Thread's Primary Token |
As you can see, the owner of this token was a member of the Administrators group (Group 02). This means if this thread had not been impersonating, it would have been granted FILE_ALL_ACCESS to the device object. Let's check out the impersonation token to see what was happening (See Figure 7).
1: kd> !token 0xb3489af0
_TOKEN b3489af0
TS Session ID: 0x3
User: S-1-5-21-1481837369-2907903028-1708812341-500
Groups:
00 S-1-5-21-1481837369-2907903028-1708812341-513
Attributes - Mandatory Default Enabled
01 S-1-1-0
Attributes - Mandatory Default Enabled
02 S-1-5-32-544
Attributes - Mandatory Default Enabled Owner
03 S-1-5-32-545
Attributes - Mandatory Default Enabled
04 S-1-5-4
Attributes - Mandatory Default Enabled
05 S-1-5-11
Attributes - Mandatory Default Enabled
06 S-1-5-15
Attributes - Mandatory Default Enabled
07 S-1-5-5-0-35361231
Attributes - Mandatory Default Enabled LogonId
08 S-1-2-0
Attributes - Mandatory Default Enabled
09 S-1-5-64-10
Attributes - Mandatory Default Enabled
10 S-1-16-12288
Attributes - GroupIntegrity GroupIntegrityEnabled
11 S-1-16-12288
Attributes - GroupIntegrity GroupIntegrityEnabledDesktop
Primary Group: S-1-5-21-1481837369-2907903028-1708812341-513
Privs:
00 0x000000017 SeChangeNotifyPrivilege Attributes - Enabled Default Authentication ID: (0,21b91ea)
Impersonation Level: Delegation
TokenType: Impersonation
Source: User32 TokenFlags: 0x11
Token ID: 21d53a4 ParentToken ID: 21d530a
Modified ID: (0, 21d53a1)
RestrictedSidCount: 1 RestrictedSids: b3489e3c
OriginatingLogonSession: 3e7
Figure 7 - How About the Impersonation Token |
At first blush, the reason why this token was rejected isn't clear. The only noticeable difference between this and the previous token is that many of the privileges had been removed, but that should not affect this thread's ability to open an object since it is still a member of the Administrators group. So what's the deal?
Check the Restricted SIDs
Down near the bottom of the !token output is where the real heart of the problem lies: this token has a restricted SID. We'll define what that is in a moment, but first let's check it out and see what's in it:
1: kd> dd b3489e3c l1
b3489e3c b3489f14
1: kd> !sid @$p
SID is: S-1-1-0
In case you don't have all of the well-known SIDs memorized, this one happens to be the WORLD, or EVERYONE, SID.
When a token has a restricted SID in it, the caller is granted access only if both the "normal" SIDs and the restricted SIDs are given access to the object. For example, if the object was modified to give WORLD read access, this caller would be allowed to open the object for read access. However, since WORLD did not appear in any Allow ACEs in the DACL, the restricted SIDs check failed and the caller was denied access. Pseudo code for how this is achieved on a very, very high level would be something akin to Figure 8.
accessToCheck = DesiredAccess;
forall user and group SIDs {
compute granted access;
}
if restricted SIDs {
accessToCheck = grantedAccess;
grantedAccess = 0;
forall restricted SIDs {
compute granted access;
}
}
if granted access == 0 {
return FALSE;
}
return TRUE;
Figure 8 - Pseudo-code for Checking SID Access |
Another Way to Get the Token
If you want to check the token of a random thread or process, the token is available from the !process command. The default/primary token for the process is listed in the process information and each impersonating thread will have its impersonation token listed. Threads that are not impersonating are listed as such. See the example from a different instance of DC2 in Figure 9.
0: kd> !process 843b9020 PROCESS 843b9020 ... Image: dc2.exe ... Token b1a9c530 ... THREAD 8445da78 ... Impersonation token: aadae030 (Level Delegation) ...
THREAD 84391cb8 ... Not impersonating
Figure 9 - Another Means to Get A Token
|
The DC2 Solution
Now that I knew what I was dealing with, I quickly found the -im switch to DC2 that turns this behavior off. I suppose if I had paid closer attention to the documentation in the first place I could have saved myself some time, but what fun would that have been?
Be sure to check out the latest version of OSR's object viewer utility,OSR's object viewer utility, ObjDir v1.4. The security information dialog has been fully revamped to provide more insight into, and a finer control over, the security applied to objects in the namespace.
This article
was printed from OSR Online http://www.osronline.com
Copyright 2017 OSR Open Systems Resources, Inc.
|